<template>
  <div class="p-6 max-w-4xl mx-auto">
    <h1 class="text-2xl font-semibold mb-4">图片文字识别（OCR）</h1>

    <div class="mb-4 p-4 border rounded bg-gray-50">
      <strong>使用说明：</strong>
      <ul class="list-disc ml-5 mt-2">
        <li>可一次选择多张图片，支持拖拽或通过按钮选择。</li>
        <li>默认使用 PaddleJS（Paddle.js）在浏览器端识别，通过 CDN 动态加载模型和脚本（无需上传到服务器）。</li>
        <li>识别语言可从下拉选择（如需中文识别请选择“中文（简体/繁体）”）。首次识别中文模型可能需要下载模型文件，耗时与网络相关。</li>
        <li>识别完成后每张图片会显示识别文本，可复制或下载为 .txt。</li>
      </ul>
      <div class="mt-2 text-sm text-gray-600">提示：第一次使用需加载识别模型到本地浏览器，可能需要一些时间，请耐心等待。</div>
    </div>

    <div class="mb-4">
      <div class="flex items-center gap-3">
        <input ref="fileInput" type="file" accept="image/*" multiple @change="onFiles" class="hidden" />
        <button class="px-3 py-2 rounded bg-blue-600 text-white" @click="triggerFile">选择图片</button>
        <button class="px-3 py-2 rounded bg-green-600 text-white" :disabled="files.length===0 || running" @click="startOCR">开始识别</button>
        <button class="px-3 py-2 rounded bg-emerald-600 text-white" :disabled="files.length===0 || running" @click="downloadAll">下载全部 .txt</button>
        <button class="px-3 py-2 rounded bg-gray-300" @click="clearAll">清除</button>
        <label class="ml-4">语言：</label>
        <select v-model="lang" class="border px-2 py-1 rounded">
          <option value="auto">自动 (默认)</option>
          <option value="chi_sim">中文（简体）</option>
          <option value="chi_tra">中文（繁体）</option>
          <option value="jpn">日语</option>
          <option value="eng">English</option>
        </select>
        <label class="ml-4">后端：</label>
        <select v-model="backendChoice" class="border px-2 py-1 rounded">
          <option value="auto">自动（优先 wasm）</option>
          <option value="wasm">wasm（推荐，避免 WebGL 读回阻塞）</option>
          <option value="cpu">cpu（更稳定，但慢）</option>
          <option value="webgl">webgl（可能最快，但会有 readback 警告）</option>
        </select>
      </div>
      <div class="mt-3 text-sm text-slate-600">提示：识别大量或高分辨率图片会占用 CPU/内存，建议分批处理。</div>
    </div>

    <div class="mb-6">
      <div class="border-dashed border-2 rounded p-6 text-center" @drop.prevent="onDrop" @dragover.prevent>
        将图片拖到此处，或通过“选择图片”按钮添加（支持多张）
      </div>
    </div>

    <div v-if="statusMessage" class="mb-4 text-sm text-gray-600">{{ statusMessage }}</div>

    <div v-if="files.length===0" class="text-center text-gray-500">还没有添加图片</div>

    <div v-else>
      <div class="grid grid-cols-1 gap-4">
        <div v-for="(f, idx) in files" :key="f.id" class="p-3 border rounded flex gap-4 items-start">
          <img :src="f.url" alt="thumb" class="w-28 h-20 object-cover rounded" />
          <div class="flex-1">
            <div class="flex justify-between items-start">
              <div class="text-sm font-medium">{{ f.name }}</div>
              <div class="text-xs text-gray-500">{{ formatSize(f.size) }}</div>
            </div>
            <div class="mt-2">
              <div v-if="f.status==='pending'" class="text-sm text-gray-600">待识别</div>
              <div v-else-if="f.status==='running'" class="text-sm text-blue-600">识别中：{{ Math.round(f.progress*100) }}%</div>
              <div v-else-if="f.status==='done'" class="text-sm text-green-600">识别完成</div>
              <div v-else-if="f.status==='error'" class="text-sm text-red-600">识别失败：{{ f.error }}</div>
            </div>

            <textarea v-model="f.text" rows="6" class="w-full mt-3 p-2 border rounded text-sm"></textarea>

            <div class="mt-2 flex gap-2">
              <button class="px-3 py-1 bg-indigo-600 text-white rounded" @click="copyText(f)">复制</button>
              <button class="px-3 py-1 bg-red-500 text-white rounded" @click="removeFile(idx)">移除</button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, onBeforeUnmount } from 'vue'

const fileInput = ref(null)
const files = reactive([])
const running = ref(false)
const lang = ref('auto')
const backendChoice = ref('auto')
const statusMessage = ref('')

let paddleOcrModule = null
async function loadPaddleOCR(desiredBackend = null) {
  // 重入保护：如果已经加载过模块或正在加载中，直接复用
  if (paddleOcrModule) return paddleOcrModule
  if (window.__paddle_ocr_load_promise) {
    await window.__paddle_ocr_load_promise
    return paddleOcrModule || window.ocr || window.paddlejs?.ocr || null
  }

  // 如果用户指定了期望后端（wasm/cpu/webgl），提前设置到全局，便于 UMD 模块在初始化时读取
  try {
    if (desiredBackend) {
      window.paddlejs = window.paddlejs || {}
      window.paddlejs.env = window.paddlejs.env || {}
      window.paddlejs.env.backend = desiredBackend
      console.info('已在全局设置期望后端：', desiredBackend)
    }
  } catch (e) {
    console.warn('设置全局 paddlejs.env 失败：', e)
  }

  const tryUrls = [
    'https://unpkg.com/@paddlejs-models/ocr?module',
    'https://unpkg.com/@paddlejs-models/ocr'
  ]

  // 保存加载过程的 promise 到全局，防止页面中多处代码并发触发重复加载
  window.__paddle_ocr_load_promise = (async () => {
    for (const url of tryUrls) {
      try {
        if (url.endsWith('?module')) {
          const mod = await import(url)
          paddleOcrModule = mod
          // 若模块导入后仍能设置 env，尝试设置一次
          try { if (desiredBackend && mod && mod.env && typeof mod.env.set === 'function') mod.env.set('backend', desiredBackend) } catch(e){}
          window.__paddle_ocr_module = paddleOcrModule
          return mod
        } else {
          // 对于 UMD 脚本，先检查页面上是否已有相关 script 或已有模块实例
          if (window.ocr || window.paddlejs?.ocr) {
            paddleOcrModule = window.ocr || window.paddlejs?.ocr
            window.__paddle_ocr_module = paddleOcrModule
            return paddleOcrModule
          }

          const existing = document.querySelector(`script[src="${url}"]`)
          if (existing) {
            // 如果已有 script 标签，等待其加载并检查全局变量
            await new Promise((resolve, reject) => {
              if (window.ocr || window.paddlejs?.ocr) return resolve()
              existing.addEventListener('load', () => resolve())
              existing.addEventListener('error', () => reject(new Error('加载已存在的 PaddleOCR 脚本失败')))
            })
            paddleOcrModule = window.ocr || window.paddlejs?.ocr || null
            if (paddleOcrModule) {
              window.__paddle_ocr_module = paddleOcrModule
              // 尝试设置后端
              try {
                if (desiredBackend && paddleOcrModule.env && typeof paddleOcrModule.env.set === 'function') {
                  paddleOcrModule.env.set('backend', desiredBackend)
                } else if (desiredBackend && typeof paddleOcrModule.setBackend === 'function') {
                  paddleOcrModule.setBackend(desiredBackend)
                }
              } catch (e) { console.warn('加载后设置后端失败：', e) }
              return paddleOcrModule
            }
          }

          await new Promise((resolve, reject) => {
            const s = document.createElement('script')
            s.src = url
            s.onload = () => {
              paddleOcrModule = window.ocr || window.paddlejs?.ocr || null
              if (!paddleOcrModule) return reject(new Error('无法从 UMD 脚本获取 OCR 模块'))
              try {
                if (desiredBackend && paddleOcrModule.env && typeof paddleOcrModule.env.set === 'function') {
                  paddleOcrModule.env.set('backend', desiredBackend)
                } else if (desiredBackend && typeof paddleOcrModule.setBackend === 'function') {
                  paddleOcrModule.setBackend(desiredBackend)
                }
              } catch (e) { console.warn('加载后设置后端失败：', e) }
              window.__paddle_ocr_module = paddleOcrModule
              resolve()
            }
            s.onerror = () => reject(new Error('加载 PaddleOCR 脚本失败'))
            document.head.appendChild(s)
          })
          return paddleOcrModule
        }
      } catch (err) {
        console.warn('尝试加载 PaddleOCR 失败：', url, err)
      }
    }
    throw new Error('无法通过 CDN 加载 PaddleJS OCR 模块（尝试过 unpkg）。请检查网络或在项目中用 npm 安装 `@paddlejs-models/ocr`。')
  })()

  try {
    const mod = await window.__paddle_ocr_load_promise
    return mod
  } finally {
    // 如果加载失败，允许后续重试（清理全局 promise）
    if (!paddleOcrModule) delete window.__paddle_ocr_load_promise
  }
}

async function applyBackendChoice(ocrMod) {
  const desired = backendChoice.value === 'auto' ? 'wasm' : backendChoice.value
  statusMessage.value = `尝试设置后端：${desired} ...`
  try {
    if (ocrMod && ocrMod.env && typeof ocrMod.env.set === 'function') {
      ocrMod.env.set('backend', desired)
      console.info('ocrMod.env.set backend ->', desired)
      statusMessage.value = `后端已设置为 ${desired}`
      return desired
    }
    if (window.paddlejs && window.paddlejs.env && typeof window.paddlejs.env.set === 'function') {
      window.paddlejs.env.set('backend', desired)
      console.info('window.paddlejs.env.set backend ->', desired)
      statusMessage.value = `后端已设置为 ${desired}`
      return desired
    }
    if (typeof ocrMod.setBackend === 'function') {
      await ocrMod.setBackend(desired)
      console.info('ocrMod.setBackend ->', desired)
      statusMessage.value = `后端已设置为 ${desired}`
      return desired
    }
  } catch (e) {
    console.warn('设置后端失败：', e)
    statusMessage.value = `设置后端 ${desired} 失败，尝试回退` + (e.message ? (': ' + e.message) : '')
  }

  // 回退到 cpu
  try {
    const fallback = 'cpu'
    if (ocrMod && ocrMod.env && typeof ocrMod.env.set === 'function') {
      ocrMod.env.set('backend', fallback)
    } else if (window.paddlejs && window.paddlejs.env && typeof window.paddlejs.env.set === 'function') {
      window.paddlejs.env.set('backend', fallback)
    } else if (typeof ocrMod.setBackend === 'function') {
      await ocrMod.setBackend(fallback)
    }
    statusMessage.value = `已回退到后端 ${fallback}`
    console.info('回退后端 ->', fallback)
    return fallback
  } catch (e) {
    console.warn('回退到 cpu 失败：', e)
    statusMessage.value = '无法设置后端，使用默认后端（可能会有 WebGL 警告）'
    return null
  }
}

function triggerFile() {
  fileInput.value && fileInput.value.click()
}

function onFiles(e) {
  const list = e.target.files || e.dataTransfer?.files
  addFiles(list)
  if (fileInput.value) fileInput.value.value = ''
}

function onDrop(e) {
  const list = e.dataTransfer.files
  addFiles(list)
}

function addFiles(list) {
  if (!list) return
  for (const file of list) {
    if (!file.type.startsWith('image/')) continue
    const id = Date.now().toString(36) + Math.random().toString(36).slice(2,8)
    const url = URL.createObjectURL(file)
    files.push({ id, file, url, name: file.name, size: file.size, status: 'pending', progress: 0, text: '', error: '' })
  }
}

function formatSize(n) {
  if (n<1024) return n + ' B'
  if (n<1024*1024) return (n/1024).toFixed(1) + ' KB'
  return (n/(1024*1024)).toFixed(2) + ' MB'
}

function loadImage(url) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    // object URLs don't need crossOrigin, but for remote urls allow anonymous
    try { img.crossOrigin = 'anonymous' } catch (e) {}
    img.onload = () => resolve(img)
    img.onerror = (e) => reject(new Error('图片加载失败'))
    img.src = url
  })
}

function removeFile(i) {
  const f = files[i]
  try { URL.revokeObjectURL(f.url) } catch(e) {}
  files.splice(i,1)
}

function clearAll() {
  for (const f of files) try { URL.revokeObjectURL(f.url) } catch(e) {}
  files.splice(0, files.length)
}

async function startOCR() {
  if (files.length === 0) return
  running.value = true
  try {
    const desiredBackend = backendChoice.value === 'auto' ? 'wasm' : backendChoice.value
    statusMessage.value = `准备加载 OCR 模块，期望后端：${desiredBackend}`
    const ocrMod = await loadPaddleOCR(desiredBackend)

    // 模块加载后再做一次运行时设置（作为补偿），然后 init
    try {
      await applyBackendChoice(ocrMod)
    } catch (e) {
      console.warn('applyBackendChoice 抛出异常：', e)
    }

    // 初始化（init 可能会下载模型并做预热工作）
    if (ocrMod.init) {
      try { await ocrMod.init() } catch (e) { console.warn('ocr.init() 失败：', e) }
    }

    // 如果模块提供 preheat，尝试调用以提前加载/准备模型，减少后续延迟
    try {
      if (typeof ocrMod.preheat === 'function') {
        statusMessage.value = '正在预热模型...'
        await ocrMod.preheat()
        statusMessage.value = ''
      }
    } catch (e) {
      console.warn('preheat 失败：', e)
    }

    for (const f of files) {
      if (f.status === 'done') continue
      f.status = 'running'
      f.progress = 0
      // 执行识别
      f.error = ''
      try {
        // paddlejs OCR expects an image/canvas element or image data.
        // Ensure we pass an HTMLImageElement: load from object URL if needed.
          // PaddleJS 的 recognize 通常期望接收 HTMLImageElement 或者 Canvas
          let imgEl
          if (f._imgEl && f._imgEl instanceof HTMLImageElement) {
            imgEl = f._imgEl
          } else {
            // loadImage 接受 URL（objectURL）并返回 HTMLImageElement
            imgEl = await loadImage(f.url)
            f._imgEl = imgEl
          }

          // 调用识别
          const res = await ocrMod.recognize(imgEl, { lang: lang.value })

          // 解析常见返回格式，兼容不同版本的返回结构
          if (!res) {
            f.text = ''
          } else if (typeof res === 'string') {
            f.text = res
          } else if (res.data) {
            const texts = []
            if (Array.isArray(res.data)) {
              res.data.forEach(d => {
                if (d.text) texts.push(d.text)
                else if (d.words) texts.push(d.words.map(w => w.text).join(''))
                else if (d.texts) texts.push(d.texts.join(''))
              })
            }
            f.text = texts.join('\n') || JSON.stringify(res)
          } else if (res.text) {
            f.text = res.text
          } else if (Array.isArray(res)) {
            f.text = res.map(r => (r.text ? r.text : JSON.stringify(r))).join('\n')
          } else {
            f.text = JSON.stringify(res)
          }

          f.status = 'done'
          f.progress = 1
          // 让出事件循环，避免长时间阻塞导致页面卡顿
          await new Promise((r) => setTimeout(r, 50))
      } catch (err) {
        f.status = 'error'
        f.error = err.message || String(err)
      }
    }
  } finally {
    running.value = false
  }
}

function copyText(f) {
  if (!f || !f.text) return
  navigator.clipboard.writeText(f.text).then(()=>{
    alert('已复制到剪贴板')
  }).catch(()=>{
    alert('复制失败，请手动复制')
  })
}

function downloadAll() {
  if (!files || files.length === 0) return
  const data = files.map(f => ({ name: f.name || '', text: f.text || '' }))
  const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'text/plain;charset=utf-8' })
  const a = document.createElement('a')
  a.href = URL.createObjectURL(blob)
  a.download = 'ocr-results.txt'
  document.body.appendChild(a)
  a.click()
  a.remove()
  // 延迟释放对象 URL，避免某些浏览器在点击时立即释放导致下载失败
  setTimeout(()=>{ try { URL.revokeObjectURL(a.href) } catch(e){} }, 2000)
}

function downloadText(f) {
  const blob = new Blob([f.text || ''], { type: 'text/plain;charset=utf-8' })
  const a = document.createElement('a')
  a.href = URL.createObjectURL(blob)
  a.download = (f.name || 'ocr') + '.txt'
  document.body.appendChild(a)
  a.click()
  a.remove()
}

onBeforeUnmount(()=>{
  for (const f of files) try { URL.revokeObjectURL(f.url) } catch(e) {}
})
</script>

<style scoped>
.list-disc { list-style: disc; }
</style>
